const ffmpeg = require('fluent-ffmpeg');
const fs = require('fs').promises;
const { Readable } = require('stream');

// 设置exe路径
ffmpeg.setFfprobePath('C:/Users/xyxy0/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-7.0.2-full_build/bin/ffprobe.exe')
ffmpeg.setFfmpegPath('C:/Users/xyxy0/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-7.0.2-full_build/bin/ffmpeg.exe')

//test
// accurateCut({
//     startTime: '00:00:01.05',
//     endTime: '00:10:00',
//     inputPath: 'C:/Users/xyxy0/Videos/input.ts'
// }).then(x=>console.log(x))

module.exports = {
    accurateCut,
}

// 精准切割
// startTime 开始时刻 ff时间格式
// endTime 结束时刻(ff中的to) ff时间格式
// inputPath为原始ts视频的文件路径
// { string|number, string|number, string } -> Promise<ffmpeg.FfprobeData>
async function accurateCut({ startTime, endTime, inputPath }) {
    //验证参数TODO

    if(toMs(startTime) == 0){
        return await outputFile(ffmpeg(inputPath)
            .audioCodec('copy')
            .videoCodec('copy')
            .outputOptions(`-to ${endTime}`)
        )
    }

    // const originStats = await fs.stat(inputPath)
    // console.log(originStats)

    //处理endTime并去除音轨
    const noAudioCommand = ffmpeg(inputPath)
        .noAudio()
        .videoCodec('copy')
        .outputOptions(`-to ${endTime}`)
    // 得到无音轨的视频属性
    const baseVideoProp = await outputFile(noAudioCommand)
    const baseFormat = baseVideoProp.format

    //test
    // const baseVideoProp = await getMetadata(inputPath)

    // 单独处理视频切割
    const videoProp = await cutVideo(baseVideoProp, startTime)
    const videoFormat = videoProp.format
    //裁剪音轨
    const noVideoCommand = ffmpeg(inputPath)
        .noVideo()
        .audioCodec('copy')
        .seekInput(startTime)
        .duration(videoFormat.duration)
        .outputFormat(baseFormat.format_name)
    const audioBuf = await runFF(noVideoCommand, baseFormat.size)
    const audioStream = uint8ArrayToStream(audioBuf)
    //合并视频与音轨
    const mergeCommand = ffmpeg(videoFormat.filename)
        .input(audioStream)
        .videoCodec('copy')
        .audioCodec('copy')
    const meidaProp = await outputFile(mergeCommand)
    //删除文件
    await fs.unlink(baseFormat.filename)
    await fs.unlink(videoFormat.filename)
    //重命名并移动文件
    // if (outputPath) await fs.rename(meidaProp.filename, outputPath)
    return meidaProp
}


// 单独处理视频切割
// (ffmpeg.FfprobeData, string|number) -> Promise<ffmpeg.FfprobeData>
async function cutVideo(baseVideoProp, startTime) {
    //切割时长的偏移量
    const offsetMs = 4000
    const startMs = toMs(startTime)
    const baseFormat = baseVideoProp.format
    //startTime后切一刀，得到bodyPart
    const bodyPartProp = await inaccurateCut(baseFormat.filename, baseFormat.duration, startMs, offsetMs)
    const bodyFormat = bodyPartProp.format
    //startTime前切一刀，得到tempPart
    const tempPartProp = await inaccurateCut(baseFormat.filename, baseFormat.duration, startMs, -offsetMs)
    const tempFormat = tempPartProp.format
    const tempStartTime = baseFormat.duration - tempFormat.duration
    console.log("tempStartTime", tempStartTime)
    //如果没有bodyPart，则tempPart解码切割，得到期望的视频
    const tempCommand = ffmpeg(tempFormat.filename).seekOutput(startMs / 1000 - tempStartTime)
    if (!bodyFormat.duration) {
        const targetProp = await outputFile(tempCommand)
        //删除文件
        await fs.unlink(bodyFormat.filename)
        await fs.unlink(tempFormat.filename)
        return targetProp
    }
    //tempPart解码切割，切到bodyPart的开始时刻，得到headPart
    const codec = tempPartProp.streams[0].codec_name == 'hevc' ? 'libx265' : 'libx264'
    const headPartProp = await outputFile(
        tempCommand.outputOptions(`-to ${tempFormat.duration - bodyFormat.duration}`)
            .videoCodec(codec)
    )
    const headFormat = headPartProp.format
    //合并 headPart + bodyPart，得到期望的视频
    // const concatTxt = randomName()+'.txt'
    // await fs.writeFile(concatTxt, "file '"+headFormat.filename + "'\nfile '"+bodyFormat.filename+"'" )
    // const targetProp = await outputFile(ffmpeg(concatTxt).inputFormat("concat").videoCodec('copy'))
    const targetProp = await outputFile(ffmpeg(`concat:${headFormat.filename}|${bodyFormat.filename}`).videoCodec('copy'))
    //删除文件
    await fs.unlink(tempFormat.filename)
    await fs.unlink(headFormat.filename)
    await fs.unlink(bodyFormat.filename)
    return targetProp
}

// 非精准切割，得到后半部分视频属性, offsetMs可以为负数
// (string, float, int, int) -> Promise<ffmpeg.FfprobeData>
async function inaccurateCut(filename, videoDuration, targetMs, offsetMs) {
    let targetTime = (targetMs + offsetMs) / 1000
    if (targetTime < 0) targetTime = 0
    const comannd = ffmpeg(filename).videoCodec('copy').seekInput(targetTime)
    const videoProp = await outputFile(comannd)
    const { format } = videoProp
    if (!format.duration) return videoProp
    const realStartMs = (videoDuration - format.duration) * 1000
    const success = offsetMs > 0 ? targetMs < realStartMs : targetMs > realStartMs
    if (success) return videoProp
    //非预期，删除文件
    await fs.unlink(format.filename);
    //递归
    return inaccurateCut(filename, videoDuration, targetMs, offsetMs * 2)
}


// 转码到文件，结束后异步返回FfprobeData
// ffmpeg.FfmpegCommand -> Promise<ffmpeg.FfprobeData>
function outputFile(command) {
    // const outputFormat = 'mpegts'
    return new Promise((resolve, reject) => {
        const tempFileName = randomName() + '.ts'
        command
            // .outputFormat(outputFormat)
            .output(tempFileName)
            .on('start', function (commandLine) {
                console.log('Spawned Ffmpeg with command: ' + commandLine);
            })
            .on('error', function (err) {
                console.log('An error occurred: ' + err.message);
                reject(err)
            })
            .on('end', async function () {
                //-ss 大于视频时长的话会输出一个0KB文件(.ts，用ffprobe会报错),如果输出格式.mp4会是个空流文件
                const { size } = await fs.stat(tempFileName)
                if (size == 0) return resolve({
                    format: {
                        filename: tempFileName,
                        duration: 0,
                    }
                })
                const metadata = await getMetadata(tempFileName)
                resolve(metadata)
            })
            .run()
    })
}

// 随机名
// void -> string
function randomName() {
    const name = 'tmp' + (new Date()).getTime() + Math.random().toFixed(6)
    return name.replace('0.', '_')
}

// 使用ffprobe获取视频文件属性
// string -> Promise<ffmpeg.FfprobeData>
function getMetadata(filePath) {
    return new Promise((resolve, reject) => {
        ffmpeg.ffprobe(filePath, function (err, metadata) {
            if (err) return reject(err)
            resolve(metadata)
        });
    })
}

// ff时间转毫秒整型
// string|number -> int
function toMs(time) {
    if (typeof time === "number")
        return parseInt(time * 1000)
    const d = new Date(`1970-01-01T${time}Z`)
    return d.getTime()
}

// 输出到内存, 无法得到内存中的视频时长，暂不使用方法处理视频
// (ffmpeg.FfmpegCommand, int) -> Promise<Uint8Array>
function runFF(command, bufSize) {
    return new Promise((resolve, reject) => {
        const arr = new Uint8Array(bufSize)
        const ffstream = command
            .on('error', function (err) {
                console.log('An error occurred: ' + err.message);
                reject(err)
            }).pipe();
        let offset = 0
        ffstream.on('data', function (chunk) {
            arr.set(chunk, offset)
            offset += chunk.length
        });
        ffstream.on('end', function () {
            console.log('Processing finished !', offset);
            resolve(arr.slice(0, offset))
        });
    })
}

// buf转读流
// Uint8Array -> Readable
function uint8ArrayToStream(uint8Array) {
    // 初始化位置
    let pos = 0
    return new Readable({
        read(size) {
            // 当流想要读取数据时，我们使用read方法
            if (pos < uint8Array.length) {
                // 通过push方法向流中传递数据
                const chunk = uint8Array.slice(pos, pos + size);
                this.push(chunk);
                pos += chunk.length;
            } else {
                // 当没有更多数据时，调用push null来结束流
                this.push(null);
            }
        }
    });
}

